#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    ui->mainToolBar->hide();

    connectionType = ctLogin;

    logEdit = new QPlainTextEdit();
    QFont font = logEdit->font();
    font.setFamily("Courier New");
    font.setPointSize(9);
    logEdit->setFont(font);

    socket = new QTcpSocket(this);
    connect(socket, SIGNAL(connected()), this, SLOT(onSocketConnected()));
    connect(socket, SIGNAL(disconnected()), this, SLOT(onSocketDisconnected()));
    connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError)));
    connect(socket, SIGNAL(readyRead()), this, SLOT(onSocketReadyRead()));

    servSocket = 0L;

    server = new QTcpServer(this);
    connect(server, SIGNAL(newConnection()), this, SLOT(onServerConnected()));
    server->listen(QHostAddress::Any, 5190);

    QVBoxLayout *layout = new QVBoxLayout();
    layout->addWidget(logEdit);
    ui->centralWidget->setLayout(layout);
}

MainWindow::~MainWindow()
{
    delete ui;
}
//---------------------------------------------------------------------------

void MainWindow::connectToServer(QString host, quint16 port)
{
    logSvcMessage("attempting to connect to "+host+":"+QString::number(port)+"...");
    socket->connectToHost(host, port);
}

void MainWindow::disconnectFromServer()
{
    socket->disconnectFromHost();
}
//---------------------------------------------------------------------------

void MainWindow::onSocketConnected()
{
    logSvcMessage("*** sniffer connected to server ***");
}

void MainWindow::onSocketDisconnected()
{
    logSvcMessage("*** sniffer disconnected from server ***");
}

void MainWindow::onSocketReadyRead()
{
    inBuffer += socket->readAll();
    do
    {
        FLAP__Header *hdr = reinterpret_cast<FLAP__Header*>(inBuffer.data());
        u16 expectedLength = qFromBigEndian(hdr->payloadLength) + sizeof(FLAP__Header);
        if (inBuffer.size() < expectedLength)
            break;
        QByteArray ba = inBuffer.left(expectedLength);
        inBuffer.remove(0, expectedLength);
        logInBytes(ba);
        parseFLAP(ba, false);
        if (servSocket)
            servSocket->write(ba);
    } while (1);
}

void MainWindow::onSocketError(QAbstractSocket::SocketError socketError)
{
}
//---------------------------------------------------------------------------

void MainWindow::onServerConnected()
{
    logSvcMessage("*** client connected to sniffer ***");
    servSocket = server->nextPendingConnection();
    connect(servSocket, SIGNAL(disconnected()), this, SLOT(onServerDisconnected()));
    connect(servSocket, SIGNAL(readyRead()), this, SLOT(onServerRead()));
    if (connectionType == ctLogin)
    {
        connectToServer("login.icq.com", 5190);
    }
    else if (connectionType == ctBOS)
    {
        if (BOS.isEmpty())
        {
            logError("BOS server address is not received");
        }
        else
        {
            QStringList list = BOS.split(':');
            QString host = list[0];
            quint16 port = list.at(1).toUShort();
            connectToServer(host, port);
        }
    }
}

void MainWindow::onServerDisconnected()
{
    if (connectionType == ctLogin)
    {
        connectionType = ctBOS;
    }
    else if (connectionType == ctBOS)
    {
        connectionType = ctLogin;
        BOS = "";
    }

    logSvcMessage("*** client disconnected from sniffer ***");
    disconnectFromServer();
    servSocket->deleteLater();
    servSocket = 0L;
}

void MainWindow::onServerRead()
{
    outBuffer += servSocket->readAll();
    do
    {
        FLAP__Header *hdr = reinterpret_cast<FLAP__Header*>(outBuffer.data());
        u16 expectedLength = qFromBigEndian(hdr->payloadLength) + sizeof(FLAP__Header);
        if (outBuffer.size() < expectedLength)
            break;
        QByteArray ba = outBuffer.left(expectedLength);
        outBuffer.remove(0, expectedLength);
        //logOutBytes(ba);
        parseFLAP(ba, true);
        socket->write(ba);
    } while (1);
}
//---------------------------------------------------------------------------

void MainWindow::logSvcMessage(QString msg)
{
    logEdit->appendHtml("<font color='gray'><pre>"+msg+"</pre></font>");
}

void MainWindow::logMessage(QString msg, QString color)
{
    if (color.isEmpty())
        color = curColor;
    else
        curColor = color;
    logEdit->appendHtml("<font color='"+color+"'><pre>"+msg+"</pre></font>");
}

void MainWindow::logOutBytes(QByteArray &ba)
{
    logEdit->appendHtml("<font color='blue'>out ("+QString::number(ba.size())+" bytes):<br>"+ba2str(ba)+"</font>");
}

void MainWindow::logInBytes(QByteArray &ba)
{
    logEdit->appendHtml("<font color='red'>in ("+QString::number(ba.size())+" bytes):<br>"+ba2str(ba)+"</font>");
}

void MainWindow::logError(QString text)
{
    logEdit->appendHtml("<font color='fuchsia'><b>*** ERROR ***: <pre>"+text+"</pre><b></font>");
}

QString MainWindow::ba2str(QByteArray &ba)
{
    QString res;
    for (int i=0; i<ba.size(); i++)
    {
        if (i)
            res += " ";
        QString byte;
        unsigned char b = ba[i];
        byte.sprintf("%02X", b);
        res += byte;
    }
    return res;
}
//---------------------------------------------------------------------------

void MainWindow::parseFLAP(QByteArray &ba, bool outcoming)
{
    QString logColor = outcoming? "blue": "red";

    logSvcMessage("parsing FLAP...");
    FLAP__Header *flap = reinterpret_cast<FLAP__Header*>(ba.data());
    QByteArray flapData = ba;
    flapData.remove(0, sizeof(FLAP__Header));

    if (flap->startMarker != '*')
    {
        logError("FLAP start marker (0x"+QString::number(flap->startMarker, 16)+") is not 0x2A");
        return;
    }

    if (flapData.size() != qFromBigEndian(flap->payloadLength))
    {
        logError("FLAP size ("+QString::number(flapData.size())+") doesn't match to expected ("+QString::number(qFromBigEndian(flap->payloadLength))+")");
        return;
    }

    QString ft;
    switch (flap->frameType)
    {
        case FLAP__FRAME_TYPE_SIGNON: ft = "New connection"; break;
        case FLAP__FRAME_TYPE_DATA: ft = "Snac data"; break;
        case FLAP__FRAME_TYPE_ERROR: ft = "Frame-level error"; break;
        case FLAP__FRAME_TYPE_SIGNOFF: ft = "Close connection"; break;
        case FLAP__FRAME_TYPE_KEEP_ALIVE: ft = "Keep alive"; break;
        default: logError("Unknown FLAP channel ("+QString::number(flap->frameType)+")"); return;
    }

    logMessage("FLAP (seq=0x"+QString::number(qFromBigEndian(flap->sequenceNumber), 16)+"): " + ft, logColor);

//    if (outcoming)
//        logOutBytes(flapData);
//    else
//        logInBytes(flapData);

    u32 flapVersion;

    switch (flap->frameType)
    {
      case FLAP__FRAME_TYPE_SIGNON:
        flapVersion = readU32(flapData);
        logMessage("FLAP version: "+QString::number(flapVersion));
        while (readTLV(flapData));
        break;

      case FLAP__FRAME_TYPE_DATA:
        readSnac(flapData);
        break;

      case FLAP__FRAME_TYPE_ERROR:
        break;

      case FLAP__FRAME_TYPE_SIGNOFF:
        while (readTLV(flapData));
        break;

      case FLAP__FRAME_TYPE_KEEP_ALIVE:
        break;
    }

    if ((connectionType == ctLogin) && (!BOS.isEmpty()))
    {
        int sz = BOS.size();
        QByteArray fakeBOS = "127.0.0.1:5190                        ";
        switch (sz)
        {
            case 14: fakeBOS = "127.0.0.1:5190"; break;
            case 15: fakeBOS = "127.0.0.01:5190"; break;
            case 16: fakeBOS = "127.0.0.001:5190"; break;
            case 17: fakeBOS = "127.0.00.001:5190"; break;
            case 18: fakeBOS = "127.0.000.001:5190"; break;
            case 19: fakeBOS = "127.00.000.001:5190"; break;
            case 20: fakeBOS = "127.000.000.001:5190"; break;
            default: fakeBOS.resize(sz);
        }
        ba.replace(BOS, fakeBOS);
    }
}

u08 MainWindow::readU08(QByteArray &ba)
{
    if ((size_t)ba.size() < sizeof(u08))
    {
        qDebug() << "cannot read u08 integer from byte array";
        return 0;
    }
    u08 res = ba[0];
    ba.remove(0, 1);
    return res;
}

u16 MainWindow::readU16(QByteArray &ba)
{
    if ((size_t)ba.size() < sizeof(u16))
    {
        qDebug() << "cannot read u16 integer from byte array";
        return 0;
    }
    u16 res = *reinterpret_cast<u16*>(ba.data());
    res = qFromBigEndian(res);
    ba.remove(0, sizeof(u16));
    return res;
}

u32 MainWindow::readU32(QByteArray &ba)
{
    if ((size_t)ba.size() < sizeof(u32))
    {
        qDebug() << "cannot read u32 integer from byte array";
        return 0;
    }
    u32 res = *reinterpret_cast<u32*>(ba.data());
    res = qFromBigEndian(res);
    ba.remove(0, sizeof(u32));
    return res;
}

QString MainWindow::readString(QByteArray &ba, int length)
{
    QString res = ba.left(length);
    ba.remove(0, length);
    return res;
}

QByteArray MainWindow::readBytes(QByteArray &ba, int length)
{
    QByteArray res = ba.left(length);
    ba.remove(0, length);
    return res;
}

TLV__Header MainWindow::readTLVHeader(QByteArray &ba)
{
    TLV__Header tlv = *reinterpret_cast<TLV__Header*>(ba.data());
    tlv.tag = qFromBigEndian(tlv.tag);
    tlv.len = qFromBigEndian(tlv.len);
    ba.remove(0, sizeof(TLV__Header));
    return tlv;
}

bool MainWindow::readTLV(QByteArray &ba)
{
    if ((size_t)ba.size() < sizeof(TLV__Header))
    {
        if (!ba.isEmpty())
            logError("TLV size is less than sizeof(TLV__Header)");
        return false;
    }

    TLV__Header tlv = readTLVHeader(ba);

    u16 tempU16;
    u32 tempU32;
    QString tempStr;
    QByteArray tempBa;

    QString msg = "TLV (tag="+QString::number(tlv.tag)+"): ";
    switch (tlv.tag)
    {
      case 0x0001: // screen name
        tempStr = readString(ba, tlv.len);
        msg += "screen name = " + tempStr;
        break;

      case 0x0002: // roasted password
        tempBa = readBytes(ba, tlv.len);
        msg += "password = " + ba2str(tempBa);
        break;

      case FLAP_SIGNON_TAG_CLIENT_NAME:
        tempStr = readString(ba, tlv.len);
        msg += "client name = " + tempStr;
        break;

      case 0x0004: // error description url
        tempStr = readString(ba, tlv.len);
        msg += "error description url = " + tempStr;
        break;

      case 0x0005: // BOS server address
        tempStr = readString(ba, tlv.len);
        BOS = tempStr;
        msg += "BOS server address = " + tempStr;
        break;

      case FLAP_SIGNON_TAG_LOGIN_COOKIE:
        tempBa = readBytes(ba, tlv.len);
        msg += "login cookie = " + ba2str(tempBa);
        break;

      case 0x0008: // error subcode
        tempU16 = readU16(ba);
        msg += "error subcode = " + QString::number(tempU16);
        break;

      case FLAP_SIGNON_TAG_CLIENT_COUNTRY:
        tempStr = readString(ba, tlv.len);
        msg += "client country = " + tempStr;
        break;

      case FLAP_SIGNON_TAG_CLIENT_LANGUAGE:
        tempStr = readString(ba, tlv.len);
        msg += "client language = " + tempStr;
        break;

      case 0x0014: // distribution number
        tempU32 = readU32(ba);
        msg += "distribution number = " + QString::number(tempU32);
        break;

      case 0x0016: // client id
        tempU16 = readU16(ba);
        msg += "client id = " + QString::number(tempU16);
        break;

      case FLAP_SIGNON_TAG_MAJOR_VERSION:
        tempU16 = readU16(ba);
        msg += "client major version = " + QString::number(tempU16);
        break;

      case FLAP_SIGNON_TAG_MINOR_VERSION:
        tempU16 = readU16(ba);
        msg += "client minor version = " + QString::number(tempU16);
        break;

      case FLAP_SIGNON_TAG_POINT_VERSION:
        tempU16 = readU16(ba);
        msg += "client point version = " + QString::number(tempU16);
        break;

      case FLAP_SIGNON_TAG_BUILD_NUM:
        tempU16 = readU16(ba);
        msg += "client build number = " + QString::number(tempU16);
        break;

      case 0x0040: // latest beta build number
        tempU32 = readU32(ba);
        msg += "latest beta build number = " + QString::number(tempU32);
        break;

      case 0x0041: // latest beta install url
        tempStr = readString(ba, tlv.len);
        msg += "latest beta install url = " + tempStr;
        break;

      case 0x0042: // latest beta info url
        tempStr = readString(ba, tlv.len);
        msg += "latest beta info url = " + tempStr;
        break;

      case 0x0043: // latest beta version
        tempStr = readString(ba, tlv.len);
        msg += "latest beta version = " + tempStr;
        break;

      case 0x0044: // latest release build number
        tempU32 = readU32(ba);
        msg += "latest release build number = " + QString::number(tempU32);
        break;

      case 0x0045: // latest release install url
        tempStr = readString(ba, tlv.len);
        msg += "latest release install url = " + tempStr;
        break;

      case 0x0046: // latest release info url
        tempStr = readString(ba, tlv.len);
        msg += "latest release info url = " + tempStr;
        break;

      case 0x0047: // latest release version
        tempStr = readString(ba, tlv.len);
        msg += "latest release version = " + tempStr;
        break;

      case 0x0048: // beta digest signature (MD5)
        tempStr = readString(ba, tlv.len);
        msg += "beta digest signature (MD5) = " + tempStr;
        break;

      case 0x0049: // release digest signature (MD5)
        tempStr = readString(ba, tlv.len);
        msg += "release digest signature (MD5) = " + tempStr;
        break;

      // SSI item TLV's
      case 0x0066:
        tempBa = readBytes(ba, tlv.len);
        msg += "awaiting authorization, data = " + ba2str(tempBa);
        break;

      case 0x00c8: // group IDs in this group
        msg += "containing items: ";
        while (!ba.isEmpty())
        {
            msg += QString::number(readU16(ba)) + ", ";
        }
        break;

      case 0x0131: // nick
        msg += "nick = " + readString(ba, tlv.len);
        break;
      // end of SSI item TLV's

      default:
        tempBa = readBytes(ba, tlv.len);
        msg += "unknown (length="+QString::number(tlv.len)+") = " + ba2str(tempBa);
    }

    logMessage(msg);
    return true;
}

SNAC__Header MainWindow::readSnacHeader(QByteArray &ba)
{
    SNAC__Header hdr = *reinterpret_cast<SNAC__Header*>(ba.data());
    hdr.id.foodgroup = qFromBigEndian(hdr.id.foodgroup);
    hdr.id.type = qFromBigEndian(hdr.id.type);
    hdr.flags = qFromBigEndian(hdr.flags);
    hdr.requestId = qFromBigEndian(hdr.requestId);
    ba.remove(0, sizeof(SNAC__Header));
    return hdr;
}

void MainWindow::readSnac(QByteArray &ba)
{
    if ((size_t)ba.size() < sizeof(SNAC__Header))
    {
        logError("SNAC size < sizeof(SNAC__Header)");
        return;
    }

    SNAC__Header hdr = readSnacHeader(ba);

//    u16 tempU16;
//    u32 tempU32;
//    QString tempStr;
//    QByteArray tempBa;

    QString msg = "SNAC (foodgroup="+QString::number(hdr.id.foodgroup)+", type="+QString::number(hdr.id.type)+"): ";

    switch (hdr.id.foodgroup)
    {
      case SNAC__FOODGROUP_OSERVICE:
        msg += "<i>OSERVICE</i> ";
        switch (hdr.id.type)
        {
          case OSERVICE__CLI_READY:
            msg += "Client ready, foodgroups info:<br>";
            while (ba.size())
            {
                u16 familyNumber = readU16(ba);
                u16 familyVersion = readU16(ba);
                u16 familyToolId = readU16(ba);
                u16 familyToolVersion = readU16(ba);
                msg += "number = " + QString::number(familyNumber)
                     + ", version = " + QString::number(familyVersion)
                     + ", tool id=" + QString::number(familyToolId)
                        + ", tool version=" + QString::number(familyToolVersion) + "<br>";
            }
            break;

          case OSERVICE__SRV_FAMILIES:
            msg += "Server starts connection, family list = ";
            while (ba.size())
            {
            u16 familyNumber = readU16(ba);
            msg += QString::number(familyNumber) + ", ";
            }
            break;
        }
        break;

      case SNAC__FOODGROUP_FEEDBAG:
        msg += "<i>FEEDBAG</i> ";
        switch (hdr.id.type)
        {
          case FEEDBAG__CLI_SSI_REQUEST:
            msg += "request server side contact list";
            break;

          case FEEDBAG__CLI_SSI_CHECKOUT:
            msg += "request changes in server side contact list ";
            msg += "(date/time = "+QString::number(readU32(ba))+", ";
            msg += "number of items = "+QString::number(readU16(ba))+")";
            break;

          case FEEDBAG__SRV_SSI_REPLY:
            logSvcMessage("*** Contact list begin ***");
            ba.remove(0, 8);
            logSvcMessage("The Fucking HACK applied (remove first 8 bytes)");
            msg += "contact list reply (version = " + QString::number(readU08(ba));
            msg += ", number of items = " + QString::number(readU16(ba));
            if (hdr.flags & SNAC__HEADER_FLAG_MORE_REPLIES_FOLLOW)
                msg += ", to be continued";
            msg += "):<br>";
            logMessage(msg);
            msg = "";
            while (readSsiItem(ba));
            logMessage("SSI list last change time = " + QString::number(readU32(ba)));
            logSvcMessage("*** Contact list end ***");
            break;

          case FEEDBAG__CLI_SSI_ACTIVATE:
            msg += "activate presence notifications";
            break;

          case FEEDBAG__CLI_SSI_ADD:
            msg += "add new item(s) to server side info: ";
            break;

          case FEEDBAG__CLI_SSI_UPDATE:
            msg += "modify item(s) in server-side info: ";
            break;

          case FEEDBAG__CLI_SSI_EDIT_BEGIN:
            msg += "begin server-side info editing ";
            break;

          case FEEDBAG__CLI_SSI_EDIT_END:
            msg += "end server-side info editing";
        }
        break;
    }

    msg += ba2str(ba);// append remainings of data (if present)
    logMessage(msg);
}

bool MainWindow::readSsiItem(QByteArray &ba)
{
    if (ba.size() <= 4)
        return false;

    u16 itemNameLength = readU16(ba);
    QString itemName = readString(ba, itemNameLength);
    u16 groupId = readU16(ba);
    u16 itemId = readU16(ba);
    u16 typeOfItem = readU16(ba);

    QString typeString;
    switch (typeOfItem)
    {
        case 0x0000: typeString = "Buddy record"; break;
        case 0x0001: typeString = "Group record"; break;
        case 0x0002: typeString = "White list record"; break;
        case 0x0003: typeString = "Black list record"; break;
        case 0x0004: typeString = "Permit/deny settings"; break;
        case 0x0005: typeString = "Presence info"; break;
        case 0x0009: typeString = "ICQ2k blabla XPEHb"; break;
        case 0x000e: typeString = "Ignore list record"; break;
        case 0x000f: typeString = "Last update date"; break;
        case 0x0010: typeString = "Non-ICQ contact"; break;
        case 0x0013: typeString = "Roster import time"; break;
        case 0x0014: typeString = "Avatar info"; break;
        case 21: typeString = "Order attribute"; break;
        case 22: typeString = "Collection of BART ids"; break;
        case 23: typeString = "Information about Account Linking prefrences"; break;
        case 24: typeString = "Account linking information"; break;
        case 25: typeString = "Non-Buddy interaction record"; break;
        case 29: typeString = "Vanity information kept at user logoff"; break;
        case 30: typeString = "User's favorite locations"; break;
        case 31: typeString = "BART PDMODE"; break;
        case 36: typeString = "ICQ Custom Emoticons"; break;
        default: typeString = "unknown";
    }

    logMessage("name = '" + itemName
               + "', group ID = " + QString::number(groupId)
               + ", item ID = " + QString::number(itemId)
               + ", type = " + typeString);

    u16 additionalDataLength = readU16(ba);
    QByteArray tlvba = readBytes(ba, additionalDataLength);
    while (readTLV(tlvba));

    return true;
}
//---------------------------------------------------------------------------
